{%mainunit bitcoin_p2p.pas}

{$IFDEF INTERFACE}
type
  TTMsg = class of TMsg;

  { TMsg is the base class for all protocol messages.
    It provides as an abstract interface that all individual messages
    will implement and also provides some common infrastructure to
    read/write the primitive data types and care about the endianness.}
  TMsg = class(TObject)

    { this constructor for incoming messages will not yet start the
      parsing itself. ParseAndProcess() must be called aferwards.
      The individual descendent classes will also have a second
      overloaded constructor that is used to create outgoing messages.}
    constructor Create(APeer: TPeer; AReceiveData: TStringStream);

    { ParseAndProcess will parse the raw data, populate the fields
      and if successful call the Peer's coresponding OnXxxx() method. }
    procedure ParseAndProcess; virtual; abstract;

    { Serialize an outgoing message and return a string that contains
      the raw data (including header) ready to be sent over the network.}
    function Serialize: String; virtual;

  strict protected
    FPeer: TPeer;
    FReceiveData: TStringStream;
    FSendData: TStringStream;

    { The ReadXxx() functions are used by descendents to implement
      their parsing and to handle all endianness conversions. They
      read data from the FReceiveData stream.}
    function ReadByte: Byte;
    function ReadWord: Word;
    function ReadDWord: DWord;
    function ReadQWord: QWord;
    function ReadVarInt: UInt64;
    procedure ReadBuffer(var Target; Count: SizeInt);
    function ReadVarStr: String;

    { The WriteXxx() procedures are used by descendents to implement
      their serializing with proper endianness handling. They will
      write to the FSendData stream}
    procedure WriteByte(AByte: Byte);
    procedure WriteWord(AWord: Word);
    procedure WriteDWord(ADWord: DWord);
    procedure WriteQWord(AQWord: QWord);
    procedure WriteVarInt(AVarInt: UInt64);
    procedure WriteBuffer(var Source; Count: SizeInt);
    procedure WriteVarStr(AStr: String);

    { Wrap the data into the header.
      This is called by the Serialize methods }
    function WrapHeader(Data: String; Short: Boolean=False): String;
  public
    { This factory will return an instance of the incoming Message}
    class function GetIncomingInstance(APeer: TPeer; ACommand: Tp2pCommand; AData: TStringStream): TMsg;

    { This static function is implemented in each message class
      and will be used by the factory to find the correct class
      to be instantiated for an incoming message }
    class function GetCommand: Tp2pCommand; virtual;
  end;

{$ENDIF}
{$IFDEF IMPLEMENTATION}

constructor TMsg.Create(APeer: TPeer; AReceiveData: TStringStream);
begin
  FPeer := APeer;
  FReceiveData := AReceiveData;
end;

function TMsg.Serialize: String;
begin
  Result :='';
end;

function TMsg.ReadByte: Byte;
begin
  if FReceiveData.Size - FReceiveData.Position >= 1 then begin
    Result := FReceiveData.ReadByte;
  end else
    raise EMessageParseBufferOverflow.Create;
end;

function TMsg.ReadWord: Word;
begin
  if FReceiveData.Size - FReceiveData.Position >= 2 then begin
    Result := {$ifdef ENDIAN_BIG}SwapEndian{$endif}(FReceiveData.ReadWord);
  end else
    raise EMessageParseBufferOverflow.Create;
end;

function TMsg.ReadDWord: DWord;
begin
  if FReceiveData.Size - FReceiveData.Position >= 4 then begin
    Result := {$ifdef ENDIAN_BIG}SwapEndian{$endif}(FReceiveData.ReadDWord);
  end else
    raise EMessageParseBufferOverflow.Create;
end;

function TMsg.ReadQWord: QWord;
begin
  if FReceiveData.Size - FReceiveData.Position >= 8 then begin
    Result := {$ifdef ENDIAN_BIG}SwapEndian{$endif}(FReceiveData.ReadQWord);
  end else
    raise EMessageParseBufferOverflow.Create;
end;

function TMsg.ReadVarInt: UInt64;
var
  First : Byte;
begin
  First := ReadByte;
  if First < $fd then exit(First);
  if First = $fd then exit(ReadWord);
  if First = $fe then exit(ReadDWord);
  if First = $ff then exit(ReadQWord);
end;

procedure TMsg.ReadBuffer(var Target; Count: SizeInt);
begin
  if FReceiveData.Size - FReceiveData.Position >= Count then begin
    FReceiveData.Read(Target, Count);
  end else
    raise EMessageParseBufferOverflow.Create;
end;

function TMsg.ReadVarStr: String;
var
  Length: UInt64;
begin
  Length := ReadVarInt;
  if Length > 0 then begin
    SetLength(Result, Length);
    ReadBuffer(Result[1], Length);
  end;
end;

procedure TMsg.WriteByte(AByte: Byte);
begin
  FSendData.WriteByte(AByte);
end;

procedure TMsg.WriteWord(AWord: Word);
begin
  FSendData.WriteWord({$ifdef ENDIAN_BIG}SwapEndian{$endif}(AWord));
end;

procedure TMsg.WriteDWord(ADWord: DWord);
begin
   FSendData.WriteDWord({$ifdef ENDIAN_BIG}SwapEndian{$endif}(ADWord));
end;

procedure TMsg.WriteQWord(AQWord: QWord);
begin
  FSendData.WriteQWord({$ifdef ENDIAN_BIG}SwapEndian{$endif}(AQWord));
end;

procedure TMsg.WriteVarInt(AVarInt: UInt64);
begin
  if AVarInt < $fd then begin
    WriteByte(AVarInt);
    exit;
  end;
  if AVarInt < $ffff then begin
    WriteByte($fd);
    WriteWord(AVarInt);
    exit;
  end;
  if AVarInt < $ffffffff then begin
    WriteByte($fe);
    WriteDWord(AVarInt);
    exit;
  end;
  WriteByte($ff);
  WriteQWord(AVarInt);
end;

procedure TMsg.WriteBuffer(var Source; Count: SizeInt);
begin
  FSendData.WriteBuffer(Source, Count);
end;

procedure TMsg.WriteVarStr(AStr: String);
begin
  if Length(AStr) > 0 then begin
    WriteVarInt(Length(AStr));
    WriteBuffer(AStr[1], Length(AStr));
  end else
    WriteByte(0);
end;

function TMsg.WrapHeader(Data: String; Short: Boolean): String;
var
  Header: TP2PHeader;
begin
  Header.Magic := MAGIC_BYTES;
  Header.Command := GetCommand;
  Header.Length := {$IFDEF ENDIAN_BIG}SwapEndian{$ENDIF}(Length(Data));
  if not Short then begin
    Header.CheckSum := CheckSum(Data[1], Length(Data));
    SetLength(Result, SizeOf(Header));
    Move(Header, Result[1], SizeOf(Header));
  end else begin
    SetLength(Result, SizeOf(Header) - SizeOf(Header.CheckSum));
    Move(Header, Result[1], SizeOf(Header) - SizeOf(Header.CheckSum));
  end;
  Result += Data;
  Log('sending: %s (%d bytes)', [GetCommand, Length(Data)]);
end;

class function TMsg.GetIncomingInstance(APeer: TPeer; ACommand: Tp2pCommand; AData: TStringStream): TMsg;
var
  ATMsg  : TTMsg;

begin
  for ATMsg in MsgTypes do begin
    if ACommand = ATMsg.GetCommand then begin
      exit(ATMsg.Create(APeer, AData));
    end;
  end;
  Result := nil;
end;

class function TMsg.GetCommand: Tp2pCommand;
begin
  Result := #0#0#0#0#0#0#0#0#0#0#0#0;
end;

{$ENDIF}
